今天想要介紹 Mutation 的一些設計上的習慣與技巧!
接續前面講的 Anemic Object 問題,在 Mutation 這邊也是一樣的。如果沒有注意,其實我們很容易設計出「資料導向」而非「行為導向」的 Mutation API 。
請注意, GraphQL 不是只是單純的 CRUD ,一開始的確可以先以 CRUD 作為出發點去設計 create
, update
, delete
等,但一個 update
可以做的事情包山包海,可以修改各項不同概念的欄位、子物件,比如一個訂單可以調整金額、調整商品規格、調整狀態、調整折扣等等,但只用一個 updateOrder
來實作這麼多功能,一方面提升後端實作的複雜度,另一方面也會造成前端理解的困難。
type Mutation {
updateOrder(
id: ID!
productsToDelete: [ID!]
productsToAdd: [ProductInput!]
productsToUpdate: [ProductInput!]
...
): Order
}
因此首先 要以商業邏輯需求來分離這些過勞的 API ,比如以更新 order 的 product 來說就可以再分成底下幾種 mutation :
deleteOrderProducts
removeOrderProducts
updateOrderProducts
addProductsToOrder
將 Mutation 依動作分得越特定越好,不必被 CRUD 的習慣給綁住。
當一個 mutation 的動作 (新增、刪除、更新) 足夠明確時,還會遇到一個問題:該如何處理 list 結構的更新呢 ? 在 Shopify 的教學文件裡面,他將 mutation 的設計以物件之間 (這邊是 Order 與 Products) 的關係分為三類型。
一次對上所有,比如 updateOrderProducts (products: [Product!]!)
。
但當數值太大時不好管理且功能太複雜。
一次對上部分,比如 deleteOrderProducts(ids: [ID!]!)
只需傳入需要改變的變量。
一次對一個,直接切分成個別的 mutation ,比如 addProductToOrder (product: Product!)
, removeProduct (id: ID!)
。
這種方式彈性最高,機動性也最佳,可以應付的場景最多。
經驗上來說,可以考量以下幾點作為選擇依據:
當資料量大、甚至使用分頁時,第一種方式就不太適合,而第二三就就比較實用 ; 但如果資料量不大且結構相對簡單很多,那第一種會是最簡單的實作方式。
一次要做大量簡單操作時,第一種跟第三種皆可以,但第二種就比較難用 ids
來表達操作內容。
在資料設計上,兩邊是否各自獨立還是有強制的依存關係。
user.orders
的 orders
必須要依附 user
底下,那就強烈建議使用第三種,因為這邊不只是改變關係也實際改變到子物件資料內容,因此越精確越好。Mutation 在命名上只要想好團隊規範,然後持續遵守就可以了。以 Shopify 為例,因為 GraphQL mutation 沒有一個很好的管理機制,幫助我們在 documentation 裡把各 domain 的 mutation 群聚起來,比如把 updateOrder
, createOrder
, removeOrder
放在一塊,因此他們 mutation 的命名規則就是: TypeName<action>
, 前面的 mutation 就會變成 orderCreate
, orderUpdate
, orderRemove
或是更細節的 orderProductAdd
。
不過這部分就見仁見智,我個人還是比較喜歡動名詞的結構,看起來比較易懂。
另外命名時跟 query 一樣越特定越好,就算日後用不到 deprecate 掉也容易,或是也可以在後面加個 V2
來表示是第二代 API 。